/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/common.hpp>
#include <baja/vars.hpp>
#include <baja/app/var_console.hpp>
#include <baja/service.hpp>
#include <boost/tokenizer.hpp>
#include <boost/token_functions.hpp>
#include <baja/observable.hpp>
#include <journal/common/settings_service.hpp>
#include <baja/app/debug_console_trace.hpp>
#include <journal/common/setting_group_table.h>

namespace journal {

namespace b = baja;
namespace a = baja::app;
namespace s = baja::storage;

class setting_group;

class setting_base : public isetting_base
{
protected:
    setting_base(		
        const std::shared_ptr<setting_group>& group,
        const std::wstring& name
        ) : _group(group), _name(name)
    {
    }

public:
    std::wstring name() { return _name; }
    virtual void reset() = 0;
    virtual void* getValueBuffer() = 0;
    virtual b::uint32 getValueBufferSize() = 0;
private:
    std::wstring _name;
    std::weak_ptr<setting_group> _group;
};

class bool_setting : public setting_base, public ibool_setting
{
public:
    bool_setting(
        const std::shared_ptr<setting_group>& group,
        const std::wstring& name,
        const bool value,
        const bool defaultValue
        ) : setting_base(group, name), _value(value), _defaultValue(defaultValue)
    {
    }

    // setting
    void reset() { _value = _defaultValue; }
    void* getValueBuffer() { return static_cast<void*>(&_value); }
    b::uint32 getValueBufferSize() { return sizeof(_value); }
    bool getValue() { return _value; }
    void setValue(bool value) { _value = value;	}

private:
    bool _value;
    bool _defaultValue;
};

class int_setting : public setting_base, public iint_setting
{
public:
    int_setting(
        const std::shared_ptr<setting_group>& group,
        const std::wstring& name,
        const int value,
        const int minValue,
        const int maxValue,
        const int changeStep,
        const int defaultValue
        ) : setting_base(group, name), _value(value), _minValue(minValue), _maxValue(maxValue),
        _changeStep(changeStep), _defaultValue(defaultValue)
    {
    }

    // setting
    void reset() { _value = _defaultValue; }
    void* getValueBuffer() { return static_cast<void*>(&_value); }
    b::uint32 getValueBufferSize() { return sizeof(_value); }
    int getValue() { return _value; }
    void setValue(int value)
    {
        if (value < _minValue)
        {
            _value = _minValue;
            return;
        }

        if (value > _maxValue)
        {
            _value = _maxValue;
            return;
        }

        _value = value;
    }

private:
    int _value;
    int _minValue;
    int _maxValue;
    int _changeStep;
    int _defaultValue;
};

class float_setting : public setting_base, public ifloat_setting
{
public:
    float_setting(
        const std::shared_ptr<setting_group>& group,
        const std::wstring& name,
        const float value,
        const float minValue,
        const float maxValue,
        const float changeStep,
        const float defaultValue
        ) : setting_base(group, name), _value(value), _minValue(minValue), _maxValue(maxValue),
        _changeStep(changeStep), _defaultValue(defaultValue)
    {
    }

    // setting
    void reset() { _value = _defaultValue; }
    void* getValueBuffer() { return static_cast<void*>(&_value); }
    b::uint32 getValueBufferSize() { return sizeof(_value); }
    float getValue() { return _value; }
    float getMinValue() { return _minValue;	}
    float getMaxValue()	{ return _maxValue;	}
    float getChangeStep() { return _changeStep;	}

    void setValue(float value)
    {
        if (value < _minValue)
        {
            _value = _minValue;
            return;
        }

        if (value > _maxValue)
        {
            _value = _maxValue;
            return;
        }

        _value = value;
    }

private:
    float _value;
    float _minValue;
    float _maxValue;
    float _changeStep;
    float _defaultValue;
};

class enum_setting : public setting_base, public ienum_setting
{
public:
    enum_setting(
        const std::shared_ptr<setting_group>& group,
        const std::wstring& name,
        const int value,
        const int* enumValues,
        const b::wchar** enumNames,
        const b::uint32 totalValues,
        const int defaultValue
        ) : setting_base(group, name), _value(value), _enumValues(enumValues), _enumNames(enumNames),
        _totalValues(totalValues), _defaultValue(defaultValue)
    {
    }

    // setting
    void reset() { _value = _defaultValue; }
    void* getValueBuffer() { return static_cast<void*>(&_value); }
    b::uint32 getValueBufferSize() { return sizeof(_value); }
    int getValue() { return _value; }
    void setValue(int value)
    {
        if (_enumValues == nullptr)
        {
            BAJA_THROW(std::logic_error("_enumValues can't be null")); // ERROR:
        }

        uint32 i = 0;
        for ( ; i < _totalValues ; i++)
        {
            if (value == _enumValues[i])
            {
                break;
            }
        }

        if (i == _totalValues)
        {
            BAJA_THROW(std::logic_error("invalid value")); // ERROR:
        }

        _value = value;
    }

private:
    int _value;
    const int* _enumValues;
    const b::wchar** _enumNames;
    b::uint32 _totalValues;
    int _defaultValue;
};

class setting_group : public std::enable_shared_from_this<setting_group>
{
public:
    setting_group(const std::shared_ptr<setting_group>& parent, const std::wstring& name) : _parent(parent), _name(name)
    {
    }

    static std::shared_ptr<setting_group> create(const std::shared_ptr<setting_group>& parent, const std::wstring& name)
    {
        std::shared_ptr<setting_group> returnValue = std::make_shared<setting_group>(parent, name);
        return returnValue;
    }

    std::wstring name() { return _name; }

    std::shared_ptr<setting_group> parent() { return _parent.lock(); }
    std::shared_ptr<b::ivar_group> varGroup() { return _varGroup; }
    void setVarGroup(const std::shared_ptr<b::ivar_group>& varGroup) { _varGroup = varGroup; }
    void addSetting(const std::shared_ptr<setting_base>& setting)
    {
        std::shared_ptr<setting_base> t = this->findSetting(setting->name().c_str());
        if (t) BAJA_THROW(std::invalid_argument("a setting with that name already exists"));
        _settings.push_back(setting);
    }

    std::shared_ptr<setting_base> setting(const int i) { return _settings[i]; }
    std::shared_ptr<setting_base> setting(const b::wchar* name)
    {
        std::shared_ptr<setting_base> returnValue = this->findSetting(name);
        if (!returnValue) BAJA_THROW(std::invalid_argument("couldn't find setting with given name"));
        return returnValue;
    }
    size_t settingCount() { return _settings.size(); }

    void addChild(const std::shared_ptr<setting_group>& child)
    {
        std::shared_ptr<setting_group> g = this->findChild(child->name().c_str());
        if (g) BAJA_THROW(std::invalid_argument("a setting group with that name already exists"));
        _children.push_back(child);
    }
    std::shared_ptr<setting_group> child(const int i) { return _children[i]; }
    std::shared_ptr<setting_group> child(const b::wchar* name, bool createIfMissing = false)
    {
        std::shared_ptr<setting_group> returnValue = this->findChild(name);
        if (!returnValue) 
        {
            if (createIfMissing)
            {
                returnValue = setting_group::create(shared_from_this(), name);
                this->addChild(returnValue);
            }
            else
            {
                BAJA_THROW(std::invalid_argument("couldn't find setting group with given name"));
            }
        }
        return returnValue;
    }
    size_t childrenCount() { return _children.size(); }

private:
    std::shared_ptr<setting_base> findSetting(const b::wchar* name)
    {
        auto it = std::find_if(
            _settings.begin(), 
            _settings.end(), 
            [&name](const std::shared_ptr<setting_base>& setting) { return setting->name().compare(name) == 0; }
        );

        if (it == _settings.end())
        {
            return nullptr;
        }

        return *it;
    }

    std::shared_ptr<setting_group> findChild(const b::wchar* name)
    {
        auto it = std::find_if(
            _children.begin(), 
            _children.end(), 
            [&name](const std::shared_ptr<setting_group>& child) { return child->name().compare(name) == 0; }
        );

        if (it == _children.end())
        {
            return nullptr;
        }

        return *it;
    }

    std::wstring _name;
    std::weak_ptr<setting_group> _parent;
    std::vector<std::shared_ptr<setting_group>> _children;
    std::vector<std::shared_ptr<setting_base>> _settings;
    std::shared_ptr<b::ivar_group> _varGroup;
};

class settings_service : public isettings_service, public a::ivar_console_observer, public std::enable_shared_from_this<settings_service>
{
public:
    settings_service() : _varConsoleObserverCookie(-1)
    {
        _cache = std::map<std::wstring, std::shared_ptr<setting_base>, std::function<bool(const std::wstring&, const std::wstring&)>>(
            [](const std::wstring& left, const std::wstring& right) 
            { 
                return left.compare(right) < 0; 
            });
    }

    static std::shared_ptr<settings_service> create(std::shared_ptr<baja::storage::iengine> dbEngine)
    {
        std::shared_ptr<settings_service> returnValue = std::make_shared<settings_service>();

        returnValue->_rootGroup = setting_group::create(nullptr, L"settings");

        // add it to the var console
        std::shared_ptr<a::ivar_console> varConsole = b::services::get<a::ivar_console>();

        if (!varConsole)
            BAJA_THROW(std::logic_error("var console service missing"));

        returnValue->_rootGroup->setVarGroup(b::createVarGroup(returnValue->_rootGroup->name().c_str()));
        varConsole->getRoot()->addChildGroup(returnValue->_rootGroup->varGroup());		// observe the var console
        returnValue->_varConsoleObserverCookie = varConsole->addObserver(std::static_pointer_cast<a::ivar_console_observer>(returnValue));
        returnValue->_settingGroupTable = journal::services::settings::db::setting_group_table::create(dbEngine);
        returnValue->_settingGroupTable->common()->initialize();
        return returnValue;
    }

    b::int32 addObserver(const std::shared_ptr<isettings_service_observer>& observer)
    {
        return _observerSource += observer;
    }

    void removeObserver(const std::shared_ptr<isettings_service_observer>& observer)
    {
        _observerSource -= observer;
    }

    void removeObserver(const b::int32 cookie)
    {
        _observerSource -= cookie;
    }

    void registerBool(const int id, const b::wchar* fullName, const bool defaultValue)
    {
        std::vector<std::wstring> parsedFullName = this->parseFullName(fullName);

        std::shared_ptr<setting_group> group = this->createGroups(parsedFullName);

        std::shared_ptr<bool_setting> s = std::shared_ptr<bool_setting>(new bool_setting(
            group,
            parsedFullName[parsedFullName.size()-1],
            defaultValue,
            defaultValue
            ));

        group->addSetting(std::static_pointer_cast<setting_base>(s));

        // add it to the var group
        group->varGroup()->addBoolVar(
            parsedFullName[parsedFullName.size()-1].c_str(),
            static_cast<bool*>(s->getValueBuffer()),
            0 // flags
            );

        if (id >= 0)
        {
            _idToFullname[id] = std::wstring(fullName);
        }
    }

    void registerInt(
        const int id,
        const b::wchar* fullName,
        const int minValue,
        const int maxValue,
        const int changeStep,
        const int defaultValue
        )
    {
        std::vector<std::wstring> parsedFullName = this->parseFullName(fullName);

        std::shared_ptr<setting_group> group = this->createGroups(parsedFullName);

        std::shared_ptr<int_setting> s = std::shared_ptr<int_setting>(new int_setting(
            group,
            parsedFullName[parsedFullName.size()-1],
            defaultValue,
            minValue,
            maxValue,
            changeStep,
            defaultValue
            ));

        group->addSetting(std::static_pointer_cast<setting_base>(s));

        // add it to the var group
        group->varGroup()->addIntVar(
            parsedFullName[parsedFullName.size()-1].c_str(),
            static_cast<int*>(s->getValueBuffer()),
            minValue,
            maxValue,
            changeStep,
            0 // flags
            );

        _idToFullname[id] = std::wstring(fullName);
    }

    void registerFloat(
        const int id,
        const b::wchar* fullName,
        const float minValue,
        const float maxValue,
        const float changeStep,
        const float defaultValue
        )
    {
        std::vector<std::wstring> parsedFullName = this->parseFullName(fullName);

        std::shared_ptr<setting_group> group = this->createGroups(parsedFullName);

        std::shared_ptr<float_setting> s = std::shared_ptr<float_setting>(new float_setting(
            group,
            parsedFullName[parsedFullName.size()-1],
            defaultValue,
            minValue,
            maxValue,
            changeStep,
            defaultValue
            ));

        group->addSetting(std::static_pointer_cast<setting_base>(s));

        // add it to the var group
        group->varGroup()->addFloatVar(
            parsedFullName[parsedFullName.size()-1].c_str(),
            static_cast<float*>(s->getValueBuffer()),
            minValue,
            maxValue,
            changeStep,
            0 // flags
            );

        _idToFullname[id] = std::wstring(fullName);
    }

    void registerEnum(
        const int id,
        const b::wchar* fullName,
        const int* enumValues,
        const b::wchar** enumNames,
        const b::uint32 totalValues,
        const int defaultValue
        )
    {
        std::vector<std::wstring> parsedFullName = this->parseFullName(fullName);

        std::shared_ptr<setting_group> group = this->createGroups(parsedFullName);

        std::shared_ptr<enum_setting> s = std::shared_ptr<enum_setting>(new enum_setting(
            group,
            parsedFullName[parsedFullName.size()-1],
            defaultValue,
            enumValues,
            enumNames,
            totalValues,
            defaultValue
            ));

        group->addSetting(std::static_pointer_cast<setting_base>(s));

        // add it to the var group
        group->varGroup()->addEnumVar(
            parsedFullName[parsedFullName.size()-1].c_str(),
            static_cast<int*>(s->getValueBuffer()),
            enumValues,
            enumNames,
            totalValues,
            0 // flags
            );

        _idToFullname[id] = std::wstring(fullName);
    }

    void setBool(const int id, const bool value)
    {
        std::shared_ptr<isetting_base> baseSetting = this->findSetting(id);
        std::shared_ptr<ibool_setting> boolSetting = std::dynamic_pointer_cast<ibool_setting>(baseSetting);

        bool oldValue = boolSetting->getValue();

        const b::wchar* fullName = _idToFullname[id].c_str();
        this->raiseBeforeSettingChange(fullName);

        boolSetting->setValue(value);

        this->raiseAfterSettingChange(fullName);

        if (oldValue != value)
        {
            this->saveSetting(fullName, baseSetting);
        }
    }

    bool getBool(const int id)
    {
        return *(static_cast<bool*>(this->findSetting(id)->getValueBuffer()));
    }

    int getInt(const int id)
    {
        return *(static_cast<int*>(this->findSetting(id)->getValueBuffer()));
    }

    void setInt(const int id, const int value)
    {
        std::shared_ptr<isetting_base> baseSetting = this->findSetting(id);
        std::shared_ptr<iint_setting> intSetting = std::dynamic_pointer_cast<iint_setting>(baseSetting);

        int oldValue = intSetting->getValue();

        const b::wchar* fullName = _idToFullname[id].c_str();
        this->raiseBeforeSettingChange(fullName);

        intSetting->setValue(value);

        this->raiseAfterSettingChange(fullName);

        if (oldValue != value)
        {
            this->saveSetting(fullName, baseSetting);
        }
    }

    float getFloat(const int id)
    {
        return *(static_cast<float*>(this->findSetting(id)->getValueBuffer()));
    }

    void setFloat(const int id, const b::float32 value)
    {
        std::shared_ptr<isetting_base> baseSetting = this->findSetting(id);
        std::shared_ptr<ifloat_setting> floatSetting = std::dynamic_pointer_cast<ifloat_setting>(baseSetting);

        float oldValue = floatSetting->getValue();

        const b::wchar* fullName = _idToFullname[id].c_str();
        this->raiseBeforeSettingChange(fullName);

        floatSetting->setValue(value);

        this->raiseAfterSettingChange(fullName);

        if (oldValue != value)
        {
            this->saveSetting(fullName, baseSetting);
        }
    }

    int getEnum(const int id)
    {
        return *(static_cast<int*>(this->findSetting(id)->getValueBuffer()));
    }

    void setEnum(const int id, const int value)
    {
        std::shared_ptr<isetting_base> baseSetting = this->findSetting(id);
        std::shared_ptr<ienum_setting> enumSetting = std::dynamic_pointer_cast<ienum_setting>(baseSetting);

        int oldValue = enumSetting->getValue();

        const b::wchar* fullName = _idToFullname[id].c_str();
        this->raiseBeforeSettingChange(fullName);

        enumSetting->setValue(value);

        this->raiseAfterSettingChange(fullName);

        if (oldValue != value)
        {
            this->saveSetting(fullName, baseSetting);
        }
    }

    std::shared_ptr<isetting_base> findSetting(const b::wchar* fullName)
    {
        std::shared_ptr<setting_base> returnValue = nullptr;

        // first try in the cache
        returnValue = _cache[fullName];
        if (!returnValue)
        {
            // now do the more expensive lookup
            std::vector<std::wstring> tokens = this->parseFullName(fullName);

            std::shared_ptr<setting_group> currentGroup = _rootGroup;

            int i;
            for (i = 0; i < static_cast<int>(tokens.size()) - 1; i++)
            {
                currentGroup = currentGroup->child(tokens[i].c_str());
            }
            returnValue = currentGroup->setting(tokens[i].c_str());

            // add to the cache
            _cache[fullName] = returnValue;
        }

        return returnValue;
    }

    std::shared_ptr<isetting_base> findSetting(const int id)
    {
        std::shared_ptr<setting_base> returnValue = nullptr;

        // first try in the cache
        returnValue = _cacheById[id];
        if (!returnValue)
        {
            const std::wstring& fullname = _idToFullname[id];

            returnValue = std::dynamic_pointer_cast<setting_base>(this->findSetting(fullname.c_str()));
            _cacheById[id] = returnValue;
        }

        return returnValue;
    }

    // isettings_service_observer
    void onBeforeVarChange(const std::shared_ptr<b::ivar_wrapper>& var, const std::shared_ptr<b::ivar_group>& group)
    {
        bool isSetting = false;
        std::wstring fullName = this->constructFullName(var, group, &isSetting);
        if (!isSetting) return;
        this->raiseBeforeSettingChange(fullName.c_str());
    }

    void onAfterVarChange(const std::shared_ptr<b::ivar_wrapper>& var, const std::shared_ptr<b::ivar_group>& group)
    {
        std::wstring fullName = this->constructFullName(var, group);
        this->raiseAfterSettingChange(fullName.c_str());
    }

    void reset();
    void save();
    void load();

private:

    std::wstring groupFullName(const std::wstring& path, const std::wstring& name);
    void saveGroup(const std::wstring& fullName, const std::shared_ptr<journal::setting_group>& group);
    void saveGroupTree(const std::wstring& path, std::shared_ptr<journal::setting_group> group);
    void saveSetting(const b::wchar* fullName, std::shared_ptr<journal::isetting_base> setting);

    void resetGroup(const std::wstring& fullName, const std::shared_ptr<journal::setting_group>& group);
    void resetGroupTree(const std::wstring& path, std::shared_ptr<journal::setting_group> group);

    void raiseBeforeSettingChange(const b::wchar* fullName)
    {
        _observerSource.raise([=](const std::shared_ptr<isettings_service_observer>& observer, void* context)
        {
            observer->onBeforeSettingChange(fullName);
        });
    }

    void raiseAfterSettingChange(const b::wchar* fullName)
    {
        _observerSource.raise([=](const std::shared_ptr<isettings_service_observer>& observer, void* context)
        {
            observer->onAfterSettingChange(fullName);
        });
    }

    void cleanup()
    {		
        std::shared_ptr<a::ivar_console> varConsole = b::services::get<a::ivar_console>();
        if (varConsole)
        {
            varConsole->removeObserver(_varConsoleObserverCookie);
            varConsole->getRoot()->removeChildGroup(_rootGroup->varGroup());
        }
        _rootGroup = nullptr;
    }

    std::wstring constructFullName(
        const std::shared_ptr<b::ivar_wrapper>& var, 
        const std::shared_ptr<b::ivar_group>& group,
        bool* isSetting = nullptr
        )
    {
        std::wstring fullName;

        std::stack<std::wstring> s;

        s.push(var->getName());

        if (isSetting) *isSetting = false;
        for (std::shared_ptr<b::ivar_group> g = group; g ; g = g->getParent())
        {
            if (g == _rootGroup->varGroup())
            {
                if (isSetting) *isSetting = true;
                break;
            }

            s.push(g->getName());
        }

        fullName.append(s.top());
        s.pop();
        while (!s.empty())
        {
            fullName.append(L"\\");
            fullName.append(s.top());
            s.pop();
        }

        return fullName;
    }
    void addSetting(const std::vector<std::wstring>& parsedFullName, const std::shared_ptr<setting_base>& setting)
    {
        (this->createGroups(parsedFullName))->addSetting(setting);
    }

    std::shared_ptr<setting_group> createGroups(const std::vector<std::wstring>& parsedFullName)
    {
        std::shared_ptr<setting_group> currentGroup = _rootGroup;
        for (int i = 0; i < static_cast<int>(parsedFullName.size()) - 1; i++)
        {
            std::wstring name = parsedFullName[i];
            currentGroup = currentGroup->child(name.c_str(), true); // create if doesn't exist

            std::shared_ptr<b::ivar_group> currentVarGroup = currentGroup->varGroup();

            // if we don't have a var group, create it now
            if (!currentVarGroup)
            {
                std::shared_ptr<b::ivar_group> varGroup = b::createVarGroup(name.c_str());
                currentGroup->setVarGroup(varGroup);
                currentGroup->parent()->varGroup()->addChildGroup(varGroup);
            }
        }

        return currentGroup;
    }

    std::vector<std::wstring> parseFullName(const std::wstring& fullName)
    {
        static const boost::char_delimiters_separator<b::wchar> sep(false, L"\\", L""); // drop delimiters
        boost::tokenizer<
            boost::char_delimiters_separator<b::wchar>, 
            std::wstring::const_iterator, 
            std::wstring> 
            tok(fullName, sep);

        std::vector<std::wstring> tokens;

        std::for_each(
            tok.begin(),
            tok.end(),
            [&tokens](const std::wstring& s) { tokens.push_back(s); }
        );

        if (tokens.size() < 1)
            BAJA_THROW(std::invalid_argument("invalid setting name"));

        return tokens;
    }

private:
    std::shared_ptr<setting_group> _rootGroup;
    b::observable_source<isettings_service_observer> _observerSource;
    std::map<
        std::wstring, 
        std::shared_ptr<setting_base>, 
        std::function<bool(const std::wstring&, const std::wstring&)>
    > _cache;

    std::map<
        int, 
        std::shared_ptr<setting_base>
    > _cacheById;

    std::map<int, std::wstring> _idToFullname;

    b::int32 _varConsoleObserverCookie;
    std::shared_ptr<journal::services::settings::db::setting_group_table> _settingGroupTable;
};

inline std::shared_ptr<isettings_service> createSettingsService(std::shared_ptr<baja::storage::iengine> dbEngine)
{
    std::shared_ptr<settings_service> returnValue = settings_service::create(dbEngine);
    return std::static_pointer_cast<isettings_service>(returnValue);
}

} // namespace journal